1、Istio Wasm插件
Istio Wasm插件
目录
[toc]
本节实战
实战名称 |
---|
🚩 实战:使用EnvoyFilter部署Wasm插件-2023.12.16(测试成功) |
🚩 实战:使用WasmPlugin部署Wasm插件-2023.12.16(测试成功) |
前言
WebAssembly(简称为 Wasm)的诞生源自前端,是一种为了解决日益复杂的 Web 前端应用以及有限的 JavaScript 性能而诞生的技术。它本身并不是一种语言,而是一种字节码标准。WASM 字节码和机器码非常接近,因此可以非常快速的装载运行。任何一种语言,都可以被编译成 WASM 字节码,然后在 WASM 虚拟机中执行,理论上,所有语言,包括 JavaScript、C、C++、Rust、Go、Java 等都可以编译成 WASM 字节码并在 WASM 虚拟机中执行。当然不仅可以嵌入浏览器增强 Web 应用,也可以应用于其他的场景。
WebAssembly 是为下列目标而生的:
- 快速、高效、可移植 —— 通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。
- 可读、可调试 —— WebAssembly 是一门低阶语言,但是它也有一种人类可读的文本格式,这允许通过手工来写代码,看代码以及调试代码。
- 保持安全 —— WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
- 不破坏网络 —— WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容。
1、Istio WASM
对于 Istio 来说,WebAssembly 也使得 Istio 的扩展能力得到了极大的提升,Isstio 从 1.12 版本开始引入 WASM 扩展 Envoy,当你需要添加 Envoy 或 Istio 不支持的自定义功能时,那么我们就可以使用 Wasm 插件,比如使用 Wasm 插件来添加自定义验证、认证、日志或管理配额等等。
首先我们再回顾下 Envoy 的过滤机制,Envoy 通过过滤器来实现各种功能,比如路由、负载均衡、TLS、认证、日志、监控等等。Envoy 提供了进程外架构、支持 L3/L4 filter、HTTP L7 filter,过滤器包括侦听器过滤器(Listener Filters)、网络过滤器(Network Filters)、HTTP 过滤器(HTTP Filters)三种类型。
侦听器过滤器
侦听器过滤器在初始连接阶段访问原始数据并操作 L4 连接的元数据。例如,TLS 检查器过滤器标识连接是否经过 TLS 加密,并解析与该连接关联的 TLS 元数据;HTTP Inspector Filter 检测应用协议是否是 HTTP,如果是的话,再进一步检测 HTTP 协议类型 (HTTP/1.x or HTTP/2) ,这两种过滤器解析到的元数据都可以和 FilterChainMatch 结合使用。
网络过滤器
网络过滤器访问和操作 L4 连接上的原始数据,即 TCP 数据包。例如,TCP 代理过滤器将客户端连接数据路由到上游主机,它还可以生成连接统计数据。此外,MySQL proxy、Redis proxy、Dubbo proxy、Thrift proxy 等都属于网络过滤器。
HTTP 过滤器
HTTP 过滤器在 L7 上运行,由网络过滤器(即 HTTP 连接管理器,HTTP Connection Manager)创建。这些过滤器用于访问、操作 HTTP 请求和响应,例如,gRPC-JSON 转码器过滤器可以为 gRPC 后端提供一个 REST API,并将请求和响应转换为相应的格式。此外,还包括 JWT、Router、RBAC 等多种过滤器。
2、WASM 插件
有很多编程语言都支持编写 WASM 插件,比如 C、C++、Rust、Go、Java 等等,这里我们以 Go 语言为例来编写一个简单的 WASM 插件,编写 WASM 的工具有 Solo.io 团队的 wasme
、tinygo
等,目前应用比较多是 tinygo,tinygo 支持的包可以查看 https:istiov1.19.3(--setprofile=demo)tinygoversion0.30.0
实验软件:
- 配置环境变量
将/root/tinygo/bin
放到PATH环境变量里去:
[root@master1 ~]#vim .bashrc……exportPATH=$PATH:/root/tinygo/bin……#生效source.bashrc
- 验证:
[root@master1 ~]#tinygo versiontinygoversion0.30.0linux/amd64(using goversion<unknown>andLLVMversion16.0.1)
当然我们也可以直接使用 docker 镜像来进行编译。
部署测试服务
- 这里先部署下要测试的服务
⚠️ 注意下
这里要启用istio!
[root@master1 ~]#cd istio-1.19.3/[root@master1 istio-1.19.3]#ls samples/httpbin/gateway-apihttpbin-gateway.yamlhttpbin-nodeport.yamlhttpbin-vault.yamlhttpbin.yamlREADME.mdsample-client
部署httpbin服务:
[root@master1 istio-1.19.3]#kubectl apply -f samples/httpbin/httpbin.yaml serviceaccount/httpbincreatedservice/httpbincreateddeployment.apps/httpbincreated[root@master1 istio-1.19.3]#kubectl apply -f samples/httpbin/httpbin-gateway.yaml gateway.networking.istio.io/httpbin-gatewaycreatedvirtualservice.networking.istio.io/httpbincreated
验证:
[root@master1 istio-1.19.3]#kubectl get po -l app=httpbinNAMEREADYSTATUSRESTARTSAGEhttpbin-86869bccff-tlcw92/2Running02m3s[root@master1 istio-1.19.3]#kubectl get svc -nistio-systemNAMETYPECLUSTER-IPEXTERNAL-IPPORT(S) AGEistio-ingressgatewayLoadBalancer10.105.233.167<pending>15021:31410/TCP,80:31666/TCP,443:32213/TCP,31400:30291/TCP,15443:31787/TCP34d#curl http:*About to connect() to 172.29.9.61 port 31666 (#0)*Trying172.29.9.61...*Connected to 172.29.9.61 (172.29.9.61) port 31666 (#0)>GET/status/200HTTP/1.1>User-Agent:curl/7.29.0>Host:172.29.9.61:31666>Accept:*/*><HTTP/1.1 200 OK<server:istio-envoy<date:Mon,11 Dec 2023 22:49:41 GMT<content-type:text/html;charset=utf-8<access-control-allow-origin:*<access-control-allow-credentials:true<content-length:0<x-envoy-upstream-service-time:3<*Connection #0 to host 172.29.9.61 left intact
浏览器效果:
- 如果是一个418码的话:(是一个茶壶哦)
[root@master1 istio-1.19.3]#curl -v http:*About to connect() to 172.29.9.61 port 31666 (#0)*Trying172.29.9.61...*Connected to 172.29.9.61 (172.29.9.61) port 31666 (#0)>GET/status/418HTTP/1.1>User-Agent:curl/7.29.0>Host:172.29.9.61:31666>Accept:*/*><HTTP/1.1 418 Unknown<server:istio-envoy<date:Mon,11 Dec 2023 22:51:49 GMT<x-more-info:http:<access-control-allow-origin:*<access-control-allow-credentials:true<content-length:135<x-envoy-upstream-service-time:3<-=[teapot]=-_...._.'_ _ `.
| ."` ^ `". _,
\_;`"---"`|//
| ;/
\_ _/
`"""`
* Connection #0 to host 172.29.9.61 left intact
- 如果此时请求一个banana的话呢?
[root@master1 istio-1.19.3]#curl -v http://172.29.9.61:31666/banana/418
* About to connect() to 172.29.9.61 port 31666 (#0)
* Trying 172.29.9.61...
* Connected to 172.29.9.61 (172.29.9.61) port 31666 (#0)
> GET /banana/418 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 172.29.9.61:31666
> Accept: */*
>
< HTTP/1.1 404 Not Found
< server: istio-envoy
< date: Mon, 11 Dec 2023 22:53:46 GMT
< content-type: text/html
< content-length: 233
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 3
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
* Connection #0 to host 172.29.9.61 left intact
[root@master1 istio-1.19.3]#curl -v http://172.29.9.61:31666/banana/200
* About to connect() to 172.29.9.61 port 31666 (#0)
* Trying 172.29.9.61...
* Connected to 172.29.9.61 (172.29.9.61) port 31666 (#0)
> GET /banana/200 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 172.29.9.61:31666
> Accept: */*
>
< HTTP/1.1 404 Not Found
< server: istio-envoy
< date: Mon, 11 Dec 2023 22:53:49 GMT
< content-type: text/html
< content-length: 233
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 3
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
* Connection #0 to host 172.29.9.61 left intact
[root@master1 istio-1.19.3]#
肯定是404失败的,因为httpbin服务本身就没提供这个服务。
- 所以,当前要实现的需求就是:
当我们需要访问
http://172.29.9.61:31666/banana/418
时,它就会帮我们把它重定向到http://172.29.9.61:31666/status/418
去。
2.编写插件
使用TinyGo开发Istio Wasm插件。
接下来我们就可以来编写一个 WASM 插件,这里我们将包含一个 Envoy 过滤器,将来自 http://service/banana/X
的请求重定向到 http://service/status/X
。
- 首先,这里我们要安装下go环境
具体安装细节见如下文章:(这里提供一键可部署脚本)
https://onedayxyy.cn/docs/go-centos-install
- 这里我们使用 Go 语言来编写插件,首先初始化项目:
[root@master1 ~]#cd istio
[root@master1 istio]#mkdir wasm-go-demo && cd wasm-go-demo
[root@master1 wasm-go-demo]#go mod init github.com/cnych/wasm-go-demo
go: creating new go.mod: module github.com/cnych/wasm-go-demo
[root@master1 wasm-go-demo]#go get github.com/tetratelabs/proxy-wasm-go-sdk
go: downloading github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
go get: added github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
[root@master1 wasm-go-demo]#ls
go.mod go.sum
[root@master1 wasm-go-demo]#cat go.mod
module github.com/cnych/wasm-go-demo
go 1.16
require github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 // indirect
特别注意:这里需要的go版本最起码为1.19以上。
- 然后创建
main.go
文件,内容如下:
[root@master1 wasm-go-demo]#ls
go.mod go.sum main.go
[root@master1 wasm-go-demo]#
package main
import (
"regexp"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)
type vmContext struct {
// 嵌入默认的 VM 上下文,这样我们就不需要重新实现所有方法
types.DefaultVMContext
}
func (ctx *vmContext) NewPluginContext(contextID uint32) types.PluginContext {
return &pluginContext{}
}
type pluginContext struct {
// 嵌入默认的插件上下文,这样我们就不需要重新实现所有方法
types.DefaultPluginContext
pattern string
replaceWith string
configData string // 保存插件的一些配置信息
}
// 注入额外的 Header
var additionalHeaders = map[string]string{
"who-am-i": "go-wasm-demo",
"injected-by": "istio-api!",
"site": "youdianzhishi.com",
"author": "阳明",
// 定义自定义的header,每个返回中都添加以上header
}
// NewHttpContext 为每个 HTTP 请求创建一个新的上下文。
func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
return &httpRegex{
contextID: contextID,
pluginContext: ctx,
}
}
// OnPluginStart 在插件被加载时调用。
func (ctx *pluginContext) OnPluginStart(pluginCfgSize int) types.OnPluginStartStatus {
proxywasm.LogWarnf("regex/main.go OnPluginStart()")
// 获取插件配置
data, err := proxywasm.GetPluginConfiguration()
if data == nil {
return types.OnPluginStartStatusOK
}
if err != nil {
proxywasm.LogWarnf("failed read plug-in config: %v", err)
return types.OnPluginStartStatusFailed
}
proxywasm.LogWarnf("read plug-in config: %s\n", string(data))
// 插件启动的时候读取配置
ctx.configData = string(data)
ctx.pattern = "banana/([0-9]*)"
ctx.replaceWith = "status/$1"
return types.OnPluginStartStatusOK
}
// OnPluginDone 在插件被卸载时调用。
func (ctx *pluginContext) OnPluginDone() bool {
proxywasm.LogWarnf("regex/main.go OnPluginDone()")
return true
}
type httpRegex struct {
// 嵌入默认的 HTTP 上下文,这样我们就不需要重新实现所有方法
types.DefaultHttpContext
// contextID 是插件上下文的 ID,它是唯一的。
contextID uint32
pluginContext *pluginContext
}
// OnHttpResponseHeaders 在收到 HTTP 响应头时调用。
func (ctx *httpRegex) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
proxywasm.LogWarnf("%d httpRegex.OnHttpResponseHeaders(%d, %t)", ctx.contextID, numHeaders, endOfStream)
// 添加 Header
for k, v := range additionalHeaders {
if err := proxywasm.AddHttpResponseHeader(k, v); err != nil {
proxywasm.LogWarnf("failed to add response header %s: %v", k, err)
}
}
//为了便于演示观察,将配置信息也加到返回头里
proxywasm.AddHttpResponseHeader("configData", ctx.pluginContext.configData)
return types.ActionContinue
}
// OnHttpRequestHeaders 在收到 HTTP 请求头时调用。
func (ctx *httpRegex) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
proxywasm.LogWarnf("%d httpRegex.OnHttpRequestHeaders(%d, %t)", ctx.contextID, numHeaders, endOfStream)
re := regexp.MustCompile(ctx.pluginContext.pattern)
replaceWith := ctx.pluginContext.replaceWith
s, err := proxywasm.GetHttpRequestHeader(":path")
if err != nil {
proxywasm.LogWarnf("Could not get request header: %v", err)
} else {
result := re.ReplaceAllString(s, replaceWith)
proxywasm.LogWarnf("path: %s, result: %s", s, result)
err = proxywasm.ReplaceHttpRequestHeader(":path", result)
if err != nil {
proxywasm.LogWarnf("Could not set request header to %q: %v", result, err)
}
}
return types.ActionContinue
}
func (ctx *httpRegex) OnHttpStreamDone() {
proxywasm.LogWarnf("%d OnHttpStreamDone", ctx.contextID)
}
func main() {
proxywasm.LogWarnf("regex/main.go main() REACHED")
// 设置 VM 上下文,这样我们就可以在插件启动时读取配置。
proxywasm.SetVMContext(&vmContext{})
}
在上面的代码中,我们主要关注 pluginContext
和 httpRegex
这两个结构体,其中 pluginContext
结构体主要用于插件的初始化,而 httpRegex
结构体主要用于处理 HTTP 请求。这里我们主要关注 OnHttpRequestHeaders
和 OnHttpResponseHeaders
这两个方法,这两个方法分别用于处理 HTTP 请求头和响应头,我们在这两个方法中添加了一些自定义的 Header,然后在 Istio 中就可以看到这些 Header 了。
3.部署插件
使用EnvoyFilter部署Wasm插件。
- 代码编写完成后,我们就可以使用
tinygo
来编译了,执行以下命令:
[root@master1 wasm-go-demo]#tinygo build -o main.wasm -scheduler=none -target=wasi main.go
[root@master1 wasm-go-demo]#ls
go.mod go.sum main.go main.wasm
上面的命令会生成一个 main.wasm
文件,这个文件就是我们的 WASM 插件,接下来我们就可以将这个插件部署到 Istio 中了。
- 我们可以将这个
main.wasm
文件放到一个 ConfigMap 中,然后挂载到 Envoy 中,这样就可以在 Envoy 中使用了,比如我们可以使用下面的命令来创建一个 ConfigMap:
kubectl create configmap new-filter --from-file=new-filter.wasm=main.wasm
- 然后接下来我们以 httpbin 为例来测试下,这里我们需要修改下 httpbin 的部署文件,将 ConfigMap 挂载到 Envoy 中。
修改后的部署文件如下所示:
[root@master1 wasm-go-demo]#kubectl edit deploy httpbin
# httpbin.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: httpbin
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
labels:
app: httpbin
service: httpbin
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
spec:
selector:
matchLabels:
app: httpbin
version: v1
template:
metadata:
labels:
app: httpbin
version: v1
annotations:
# 不能在容器上使用 volume 挂载,因为它来自 injector。
# NOTE: 我们这个示例始终挂在 "new-filter" ConfigMap 到 /var/local/wasm/new-filter.wasm
sidecar.istio.io/userVolume: '[{"name":"new-filter","configMap":{"name":"new-filter"}}]'
sidecar.istio.io/userVolumeMount: '[{"mountPath":"/var/local/wasm","name":"new-filter"}]'
spec:
serviceAccountName: httpbin
containers:
- image: docker.io/kennethreitz/httpbin
imagePullPolicy: IfNotPresent
name: httpbin
ports:
- containerPort: 80
查看:
[root@master1 wasm-go-demo]#kubectl get po httpbin-59b9d799cd-hrtbh -oyaml
……
- mountPath: /var/local/wasm
name: new-filter
……
- configMap:
defaultMode: 420
name: new-filter
name: new-filter
……
[root@master1 wasm-go-demo]#kubectl exec httpbin-59b9d799cd-hrtbh -c istio-proxy -- ls /var/local/wasm
new-filter.wasm
- 部署完成后接下来我们先访问下 httpbin 服务,看下是否正常:
[root@master1 wasm-go-demo]#export GATEWAY_URL=$(kubectl get po -l istio=ingressgateway -n istio-system -o 'jsonpath={.items[0].status.hostIP}'):$(kubectl get svc istio-ingressgateway -n istio-system -o 'jsonpath={.spec.ports[?(@.name=="http2")].nodePort}')
[root@master1 wasm-go-demo]#curl -v http://$GATEWAY_URL/status/418
* About to connect() to 172.29.9.63 port 31666 (#0)
* Trying 172.29.9.63...
* Connected to 172.29.9.63 (172.29.9.63) port 31666 (#0)
> GET /status/418 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 172.29.9.63:31666
> Accept: */*
>
< HTTP/1.1 418 Unknown
< server: istio-envoy
< date: Tue, 12 Dec 2023 23:12:38 GMT
< x-more-info: http://tools.ietf.org/html/rfc2324
< access-control-allow-origin: *
< access-control-allow-credentials: true
< content-length: 135
< x-envoy-upstream-service-time: 11
<
-=[ teapot ]=-
_...._
.' _ _ `.|."` ^ `". _,\_;`"---"`||;/\_ _/`"""`*Connection #0 to host 172.29.9.63 left intact[root@master1 wasm-go-demo]#
- 上面我们编写的插件逻辑是当我们访问
http:*About to connect() to 172.29.9.63 port 31666 (#0)*Trying172.29.9.63...*Connected to 172.29.9.63 (172.29.9.63) port 31666 (#0)>GET/banana/418HTTP/1.1>User-Agent:curl/7.29.0>Host:172.29.9.63:31666>Accept:*/*><HTTP/1.1 404 Not Found<server:istio-envoy<date:Tue,12 Dec 2023 23:13:09 GMT<content-type:text/html<content-length:233<access-control-allow-origin:*<access-control-allow-credentials:true<x-envoy-upstream-service-time:25<<!DOCTYPEHTMLPUBLIC"- <title>404 Not Found</title><h1>Not Found</h1><p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>*Connection #0 to host 172.29.9.63 left intact[root@master1 wasm-go-demo]#